﻿using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Configuration;
using System.Text;
using System.Web;
using System.Net;
using System.IO;

namespace TwitterDotNet.Core
{
    /// <summary>
    /// 認証を管理するクラス
    /// </summary>
    public class Oauth
    {
        const string REQUEST_TOKEN_URL = "http://twitter.com/oauth/request_token";
        const string ACCESS_TOKEN_URL = "http://twitter.com/oauth/access_token";
        const string AUTHORIZE_URL = "http://twitter.com/oauth/authorize";

        private Random random = new Random();

        /// <summary>
        /// カスタマーキーを取得
        /// </summary>
        public string ConsumerKey { get; private set; }
        /// <summary>
        /// カスタマーシークレットを取得
        /// </summary>
        public string ConsumerSecret { get; private set; }
        /// <summary>
        /// リクエストトークンを取得
        /// </summary>
        public string RequestToken { get; private set; }
        /// <summary>
        /// リクエストトークンシークレットを取得
        /// </summary>
        public string RequestTokenSecret { get; private set; }
        /// <summary>
        /// アクセストークンを取得
        /// </summary>
        public string AccessToken { get; private set; }
        /// <summary>
        /// アクセストークンシークレットを取得
        /// </summary>
        public string AccessTokenSecret { get; private set; }
        /// <summary>
        /// ユーザーIDを取得
        /// </summary>
        public string UserId { get; private set; }
        /// <summary>
        /// スクリーンネームを取得
        /// </summary>
        public string ScreenName { get; private set; }

        /// <summary>
        /// コンストラクタ
        /// 初回用
        /// </summary>
        /// <param name="consumerkey">カスタマーキー</param>
        /// <param name="consumersecret">カスタマーキーシークレット</param>
        internal Oauth(string consumerkey, string consumersecret)
        {
            ServicePointManager.Expect100Continue = false;
            ConsumerKey = consumerkey;
            ConsumerSecret = consumersecret;
        }

        /// <summary>
        /// コンストラクタ
        /// 2回目以降用
        /// </summary>
        /// <param name="consumerkey">カスタマーキー</param>
        /// <param name="consumersecret">カスタマーキーシークレット</param>
        /// <param name="accesstoken">アクセストークン</param>
        /// <param name="accesstokensecret">アクセストークンシークレット</param>
        /// <param name="userid">ユーザーID</param>
        /// <param name="screenname">スクリーンネーム</param>
        internal Oauth(string consumerkey, string consumersecret, string accesstoken, string accesstokensecret, string userid, string screenname) : this(consumerkey, consumersecret)
        {
            AccessToken = accesstoken;
            AccessTokenSecret = accesstokensecret;
            UserId = userid;
            ScreenName = screenname;
        }

        /// <summary>
        /// リクエストトークンを取得するメソッド
        /// </summary>
        public void GetRequestToken()
        {
            SortedDictionary<string, string> parameters = GenerateParameters("");
            string signature = GenerateSignature("", "GET", REQUEST_TOKEN_URL, parameters);
            parameters.Add("oauth_signature", UrlEncode(signature));
            string response = HttpGet(REQUEST_TOKEN_URL, parameters);
            try
            {
                Dictionary<string, string> dic = ParseResponse(response);
                RequestToken = dic["oauth_token"];
                RequestTokenSecret = dic["oauth_token_secret"];
            }
            catch(Exception e)
            {
                throw new TwitterAccountException("", e);
            }
        }

        /// <summary>
        /// 認証ページを取得するメソッド
        /// </summary>
        /// <returns></returns>
        public string GetAuthorizeUrl()
        {
            return AUTHORIZE_URL + "?oauth_token=" + RequestToken;
        }

        /// <summary>
        /// 認証番号からアクセストークンを取得するメソッド
        /// </summary>
        /// <param name="pin">認証番号</param>
        public void GetAccessToken(string pin)
        {
            SortedDictionary<string, string> parameters = GenerateParameters(RequestToken);
            parameters.Add("oauth_verifier", pin);
            string signature = GenerateSignature(RequestTokenSecret, "GET", ACCESS_TOKEN_URL, parameters);
            parameters.Add("oauth_signature", UrlEncode(signature));
            string response = HttpGet(ACCESS_TOKEN_URL, parameters);
            try
            {
                Dictionary<string, string> dic = ParseResponse(response);
                AccessToken = dic["oauth_token"];
                AccessTokenSecret = dic["oauth_token_secret"];
                UserId = dic["user_id"];
                ScreenName = dic["screen_name"];
            }
            catch (Exception e)
            {
                throw new TwitterAccountException("", e);
            }
        }

        /// <summary>
        /// GETするためのメソッド
        /// </summary>
        /// <param name="url">URL</param>
        /// <param name="parameters">パラメータ</param>
        /// <returns></returns>
        public string Get(string url, IDictionary<string, string> parameters)
        {
            SortedDictionary<string, string> parameters2 = GenerateParameters(AccessToken);
            if (parameters != null)
                foreach (var p in parameters)
                    parameters2.Add(p.Key, p.Value);
            string signature = GenerateSignature(AccessTokenSecret, "GET", url, parameters2);
            parameters2.Add("oauth_signature", UrlEncode(signature));
            return HttpGet(url, parameters2);
        }

        /// <summary>
        /// POSTするためのメソッド
        /// </summary>
        /// <param name="url">URL</param>
        /// <param name="parameters">パラメータ</param>
        /// <returns></returns>
        public string Post(string url, IDictionary<string, string> parameters)
        {
            SortedDictionary<string, string> parameters2 = GenerateParameters(AccessToken);
            if (parameters != null)
                foreach (var p in parameters)
                    parameters2.Add(p.Key, p.Value);
            string signature = GenerateSignature(AccessTokenSecret, "POST", url, parameters2);
            parameters2.Add("oauth_signature", UrlEncode(signature));
            return HttpPost(url, parameters2);
        }

        /// <summary>
        /// HTTPGETするためのメソッド
        /// </summary>
        /// <param name="url">URL</param>
        /// <param name="parameters">パラメータ</param>
        /// <returns></returns>
        internal string HttpGet(string url, IDictionary<string, string> parameters)
        {
            WebRequest req = null;
            WebResponse res = null;
            Stream stream = null;
            StreamReader reader = null;
            string result;
            try
            {
                req = WebRequest.Create(url + '?' + JoinParameters(parameters));
                res = req.GetResponse();
                stream = res.GetResponseStream();
                reader = new StreamReader(stream);
                result = reader.ReadToEnd();
            }
            catch (Exception e)
            {
                throw new TwitterWebException("Exception in HttpGet-Method", e);
            }
            finally
            {
                if(reader != null) reader.Close();
                if(stream != null) stream.Close();
            }
            return result;
        }

        /// <summary>
        /// HTTPPOSTするためのメソッド
        /// </summary>
        /// <param name="url">URL</param>
        /// <param name="parameters">パラメータ</param>
        /// <returns></returns>
        internal string HttpPost(string url, IDictionary<string, string> parameters)
        {
            byte[] data;
            WebRequest req;
            Stream reqstream = null;
            WebResponse res;
            Stream resstream = null;
            StreamReader reader = null;
            string result;
            try
            {
                data = Encoding.ASCII.GetBytes(JoinParameters(parameters));
                req = WebRequest.Create(url);
                req.Method = "POST";
                req.ContentType = "application/x-www-form-urlencoded";
                req.ContentLength = data.Length;
                reqstream = req.GetRequestStream();
                reqstream.Write(data, 0, data.Length);
                res = req.GetResponse();
                resstream = res.GetResponseStream();
                reader = new StreamReader(resstream, Encoding.UTF8);
                result = reader.ReadToEnd();

            }
            catch (Exception e)
            {
                throw new Exception("Exception in HttpPost-Method", e);
            }
            finally
            {
                if (reqstream != null) reqstream.Close();
                if(reader != null) reader.Close();
                if(resstream != null) resstream.Close();
            }
            return result;
        }

        /// <summary>
        /// stringからDictionaryを生成するためのメソッド
        /// </summary>
        /// <param name="response">パラメータ文字列</param>
        /// <returns></returns>
        private Dictionary<string, string> ParseResponse(string response)
        {
            Dictionary<string, string> result = new Dictionary<string, string>();
            foreach (string s in response.Split('&'))
            {
                int index = s.IndexOf('=');
                if (index == -1)
                    result.Add(s, "");
                else
                    result.Add(s.Substring(0, index), s.Substring(index + 1));
            }
            return result;
        }

        /// <summary>
        /// パラメータからstringを生成するメソッド
        /// </summary>
        /// <param name="parameters">Dictionary</param>
        /// <returns></returns>
        private string JoinParameters(IDictionary<string, string> parameters)
        {
            StringBuilder result = new StringBuilder();
            bool first = true;
            foreach (var parameter in parameters)
            {
                if (first)
                    first = false;
                else
                    result.Append('&');
                result.Append(parameter.Key);
                result.Append('=');
                result.Append(parameter.Value);
            }
            return result.ToString();
        }

        /// <summary>
        /// シグニチャを生成するメソッド
        /// </summary>
        /// <param name="tokenSecret">トークンシークレット</param>
        /// <param name="httpMethod">HTTPMETHOD</param>
        /// <param name="url">URL</param>
        /// <param name="parameters">パラメータ</param>
        /// <returns></returns>
        private string GenerateSignature(string tokenSecret, string httpMethod, string url, SortedDictionary<string, string> parameters)
        {
            string signatureBase = GenerateSignatureBase(httpMethod, url, parameters);
            HMACSHA1 hmacsha1 = new HMACSHA1();
            hmacsha1.Key = Encoding.ASCII.GetBytes(UrlEncode(ConsumerSecret) + '&' + UrlEncode(tokenSecret));
            byte[] data = System.Text.Encoding.ASCII.GetBytes(signatureBase);
            byte[] hash = hmacsha1.ComputeHash(data);
            return Convert.ToBase64String(hash);
        }

        private string GenerateSignatureBase(string httpMethod, string url, SortedDictionary<string, string> parameters)
        {
            StringBuilder result = new StringBuilder();
            result.Append(httpMethod);
            result.Append('&');
            result.Append(UrlEncode(url));
            result.Append('&');
            result.Append(UrlEncode(JoinParameters(parameters)));
            return result.ToString();
        }

        /// <summary>
        /// 辞書をソートするためのメソッド
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        internal SortedDictionary<string, string> GenerateParameters(string token)
        {
            SortedDictionary<string, string> result = new SortedDictionary<string, string>();
            result.Add("oauth_consumer_key", ConsumerKey);
            result.Add("oauth_signature_method", "HMAC-SHA1");
            result.Add("oauth_timestamp", GenerateTimestamp());
            result.Add("oauth_nonce", GenerateNonce());
            result.Add("oauth_version", "1.0");
            if (!string.IsNullOrEmpty(token))
                result.Add("oauth_token", token);
            return result;
        }

        /// <summary>
        /// URLエンコードするメソッド
        /// </summary>
        /// <param name="value">元の文字列</param>
        /// <returns></returns>
        public string UrlEncode(string value)
        {
            string unreserved = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
            StringBuilder result = new StringBuilder();
            byte[] data = Encoding.UTF8.GetBytes(value);
            foreach (byte b in data)
            {
                if (b < 0x80 && unreserved.IndexOf((char)b) != -1)
                    result.Append((char)b);
                else
                    result.Append('%' + String.Format("{0:X2}", (int)b));
            }
            return result.ToString();
        }

        /// <summary>
        /// Nounceを自動生成するメソッド
        /// </summary>
        /// <returns></returns>
        private string GenerateNonce()
        {
            string letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
            StringBuilder result = new StringBuilder(8);
            for (int i = 0; i < 8; ++i)
                result.Append(letters[random.Next(letters.Length)]);
            return result.ToString();
        }

        /// <summary>
        /// タイムスタンプを生成するメソッド
        /// </summary>
        /// <returns></returns>
        private string GenerateTimestamp()
        {
            TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
            return Convert.ToInt64(ts.TotalSeconds).ToString();
        }
    }

}
